O HotXLS mapeia cores RGB e de tema quaisquer sobre a paleta de cores de 56 entradas do BIFF8 em duas camadas: NearestIndexedColor acha a entrada existente da paleta perceptualmente mais próxima no espaço OKLab, e BuildBiffPalettePlan com ApplyBiffPalettePlan reescreve os slots livres da paleta para que uma pasta de trabalho true color sobreviva a um save para o XLS clássico. O gatilho é sempre o mesmo ticket de suporte. Alguém monta um relatório em XLSX com cabeçalhos azul-marinho corporativos e um detalhe em verde-água suave, salva como .xls para um consumidor legado, e os cabeçalhos voltam preto puro enquanto o verde-água vira um turquesa berrante. Nada travou e nenhum aviso disparou. O modelo de cores do formato velho simplesmente não comporta o que o novo descreveu, e a biblioteca tinha que escolher algo
Por que um arquivo XLS só comporta 56 cores?
Porque um formato de célula BIFF8 nunca guarda um valor RGB: fontes, preenchimentos e bordas carregam um índice de cor, e o registro Palette global da pasta de trabalho ($0092, [MS-XLS] §2.4.188) fornece exatamente 56 entradas RGB opacas para os índices 8 a 63. Os índices 0 a 7 são cópias fixas das oito cores básicas, e os valores acima de 63 nem são cores, mas tokens como cor de primeiro plano do sistema, cor de fundo do sistema e texto de gráfico. O HotXLS expõe a paleta por um ColorIndex público de 1 a 56, que é o índice físico menos 7, e o ResolveIndexedColor mantém os três esquemas de numeração separados por meio do TXLSIndexedColorSpace: xicsPublicColorIndex para os valores de API 1..56, xicsBiffIcv para os índices crus do disco, validados contra o subconjunto IcvFont, IcvXF ou IcvChart do papel que você passar, e xicsOoxmlIndexed, em que 64 e 65 significam primeiro e segundo plano do sistema
var
Res: TXLSIndexedColorResolution;
begin
// $40 é um token icv do BIFF, não um slot da paleta
Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
case Res.Kind of
xickPalette: UseArgb(Res.ARGB); // slot da paleta, se resolvido
xickAutomatic,
xickSystem: UseSystemColor(Res.SystemColorRole);
xickInvalid: RejectToken(Res.RawIndex);
end;
end;
Note que o exemplo comuta em Res.Kind e ignora o valor de retorno Boolean. O ResolveIndexedColor só devolve True quando obteve um ARGB concreto, e a sobrecarga curta nunca lê o desktop do Windows, então um token automático ou de sistema legitimamente volta False ainda classificado como xickSystem. O HotXLS esbarrou nisso no próprio serializador de pasta de trabalho: código que trata False como "sem cor" joga fora silenciosamente o significado Automatic e System do token. Se você precisa de valores RGB reais para esses tokens, chame a sobrecarga longa e forneça um callback TXLSTryResolveSystemColor que aplique sua própria política de UI, export ou headless
Por que o HotXLS casa cores em OKLab em vez de RGB?
Porque os valores de canal sRGB são codificados com gamma, então a distância euclidiana em RGB não acompanha o que uma pessoa vê, e o erro é pior justamente nos tons escuros e saturados de que as paletas corporativas adoram. Pegue o azul escuro $000033. Em RGB a distância até o preto é 51 e até a entrada navy padrão $000080 é 77, então um casador RGB pinta seu cabeçalho de preto com toda a confiança. Em OKLab as distâncias ao quadrado são cerca de 0.0312 até o preto e 0.0235 até o navy, e o HotXLS escolhe navy, ColorIndex 11 no slot físico 18; esse caso exato está fixado na suíte de testes para o motor Classic e o XLSX. A conversão dentro do ArgbToOklab lineariza cada canal sRGB, aplica a matriz LMS do OKLab, tira raízes cúbicas e projeta em L, a e b, depois do que uma distância euclidiana simples ao quadrado é uma proxy razoável de diferença percebida. OKLab não é CIEDE2000 e não finge que é, mas não tem correções de matiz por partes, custa um punhado de multiplicações por cor e é estável o bastante para comandar um loop de clustering, e é aí que ele realmente ganha seu lugar
O que o NearestIndexedColor garante?
O NearestIndexedColor garante uma resposta determinística e somente leitura: uma conversão de entrada, uma varredura fixa sobre 56 entradas em cache, e o menor índice público sempre que duas entradas empatam em proximidade. Cada pasta de trabalho faz cache do ARGB normalizado e das coordenadas OKLab dos 56 slots físicos junto com um contador de geração da paleta. Um reset da paleta reconstrói o cache, uma mudança em um único slot atualiza só esse slot, e uma consulta contra uma geração obsoleta devolve False em vez de chutar. A varredura usa comparação estrita de menor começando no slot 8, e é por isso que uma paleta contendo a mesma cor duas vezes sempre responde com o índice menor; isso importa quando você faz diff de dois arquivos gerados e espera saída byte-idêntica. O alpha de entrada segue um contrato estreito: um byte alpha zero é tratado como opaco, e um valor parcialmente transparente é rejeitado com ColorIndex 0 e PaletteSlot -1, já que entradas de paleta não têm alpha. Os writers de preenchimento e borda do motor Classic convertem RGB e cores de tema num índice com a mesma rotina de casamento OKLab na hora do save, então a API e o arquivo armazenado concordam sobre em que slot uma cor cai
var
Match: TXLSNearestIndexedColorMatch;
begin
if Workbook.NearestIndexedColor($FF000033, Match) then
begin
// Match.ColorIndex = 11, Match.PaletteSlot = 18, Match.ARGB = $FF000080
if not Match.ExactMatch then
LogApproximation(Match.InputARGB, Match.ARGB, Match.DistanceSquared);
end;
end;
Como o BuildBiffPalettePlan encaixa true colors em 56 slots?
O BuildBiffPalettePlan calcula uma proposta completa para os 56 slots sem tocar na pasta de trabalho, então você pode inspecionar, logar ou descartar. O planner primeiro chama ScanIndexedColorUsage: todo slot que uma fonte, preenchimento, borda, formatação condicional, shape, comentário ou linha de grade da planilha referenciar por índice fica travado, porque mudar uma entrada da paleta recolore de uma vez todo consumidor daquele índice. Os alvos são os RGB diretos e as cores de tema resolvidas vindas de fontes, preenchimentos, bordas, estilos diferenciais, data bars e color scales. Cada alvo é ponderado pelo maior entre sua contagem de referências renderizadas e sua contagem de definições, e uma formatação condicional conta as células que seus intervalos cobrem, então uma cor pintada numa coluna inteira pesa mais que uma usada numa única nota. O posicionamento então segue numa ordem fixa:
- Slots travados mantêm a cor de origem incondicionalmente
- Um alvo que já existe na paleta é retido no seu slot correspondente mais baixo e esse slot vira fixo
- Se os alvos únicos restantes cabem nos slots livres, cada um ganha um slot exato, atribuído em ordem crescente de ARGB
- Caso contrário
Quantizedé setado, cada slot livre é semeado com o alvo cuja distância até o centro existente mais próximo, multiplicada pelo peso, é a maior, e até 16 rodadas de k-means ponderado por frequência em OKLab movem só os centros livres até as atribuições pararem de mudar
Seja honesto consigo mesmo sobre o que o caminho de overflow entrega. O clustering é uma otimização local limitada, não um ótimo global, e um slot livre acaba guardando um centróide convertido de volta para sRGB com clamping, que pode ser uma cor que nenhuma célula usou verbatim. O que você ganha é repetibilidade: a mesma pasta de trabalho sempre produz o mesmo plano, e o plano relata o próprio estrago por meio de WeightedError, MaxDistanceSquared, ExactTargetWeight e TotalTargetWeight, então um job em lote pode se recusar a salvar quando a aproximação fica grosseira demais para um guia de marca
var
Plan: TXLSBiffPalettePlan;
I: Integer;
begin
Plan := Workbook.BuildBiffPalettePlan; // somente leitura
if Plan.Quantized and (Plan.MaxDistanceSquared > MaxAcceptedError) then
raise Exception.Create('Too many distinct colors for a BIFF8 palette');
for I := 0 to High(Plan.Slots) do
if Plan.Slots[I].Changed then
LogSlot(Plan.Slots[I].ColorIndex, Plan.Slots[I].SourceARGB,
Plan.Slots[I].TargetARGB);
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
Como o ApplyBiffPalettePlan rejeita um plano obsoleto?
O ApplyBiffPalettePlan valida o plano inteiro antes de gravar um único slot, e devolve False com a paleta intocada se algo discordar da pasta de trabalho atual. O plano carrega SourcePaletteGeneration e SourcePaletteHash, um hash FNV-1a de 64 bits sobre as 56 cores de origem; a validação também reconfere todo índice público e físico, toda cor de origem, que nenhum slot travado esteja marcado como mudado, as contagens de travados e mudados, e que todo alvo seja opaco. Qualquer mudança efetiva de paleta no meio do caminho, incluindo uma aplicação bem-sucedida anterior do mesmo plano, torna o plano obsoleto, então planos são efetivamente de uso único. Um plano válido sem slots mudados tem sucesso sem avançar a geração, e uma mudança real incrementa a geração uma vez e reconstrói o casador OKLab uma vez, no motor Classic reescrevendo o array fixo da paleta e no motor XLSX trocando por uma lista de override de cores indexadas preparada
Ligando isso para saves BIFF8 e conversão de XLSX para XLS
A propriedade BiffPaletteSavePolicy tem xbpsPreserve como padrão, então atualizar o HotXLS nunca reescreve a paleta de ninguém pelas costas. Setá-la para xbpsOptimizeTrueColors faz uma pasta de trabalho Classic montar e aplicar um plano novo dentro do SaveAs, mas só quando o formato de destino é xlExcel97; BIFF5, CSV, HTML, PDF, XLSX e os demais writers ignoram a configuração. Depois de um save bem-sucedido a paleta otimizada fica no modelo da pasta de trabalho, então consultas e saves posteriores veem o mesmo mapeamento. Se o save falha ou é cancelado, as 56 cores originais e a geração original são restauradas. Para fontes XLSX, o SaveXLSXWorkbookAsXLS em lxXlsxExport monta um plano a partir da pasta de trabalho carregada e o grava na paleta de destino antes que qualquer estilo seja convertido, que é a ponte determinística que a demo de workbench de auditoria e conversão de pastas de trabalho exercita. Cores de tema passam pelo mesmo planner depois que o tint delas é resolvido para RGB; se você prefere manter temas vivos em preenchimentos de gráfico, o artigo sobre cores de tema GelFrame em preenchimentos de gráfico cobre como o XLS binário guarda um índice de esquema em vez de uma cor achatada
// Pasta de trabalho Classic: opt in, apenas BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // paleta já restaurada
// Modelo XLSX para BIFF8 com um plano de paleta determinístico
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
XWorkbook.Free;
end;
As APIs de paleta do HotXLS funcionam do mesmo jeito em IXLSWorkbook e TXLSXWorkbook, tanto do Delphi quanto do C++Builder. Baixe a versão trial e aponte para a sua planilha mais colorida a partir da página do componente Excel HotXLS para Delphi