O azul escuro $000033 fica a 51 unidades do preto e a 77 unidades da entrada navy clássica $000080 quando a distância é medida como distância euclidiana simples sobre os valores de canal RGB. Um conversor que grava arquivos .xls clássicos com casamento nearest-RGB pinta, portanto, uma linha de cabeçalho azul escuro de preto, e preto é o que todo leitor que vai apenas pelo índice da paleta exibe. O formato não deixa espaço para o escritor: um formato de célula guarda um índice de paleta, e a paleta tem 56 entradas. Acertar o cabeçalho exige duas coisas, uma medida de distância que acompanhe o que as pessoas veem, e um jeito de reescrever a própria paleta quando uma pasta de trabalho usa mais cores distintas do que a paleta comporta
De onde vem o limite de 56 cores
Um record BIFF8 de fonte, preenchimento ou borda nunca carrega um valor RGB. Ele carrega um índice de cor, e o record 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. Valores acima de 63 nem são cores, mas tokens: $40 é o primeiro plano do sistema, $41 o fundo do sistema, $4D a $4F são texto de gráfico, fundo de gráfico e a cor neutra de gráfico, e $7FFF significa automático. O Excel 2007 acrescentou o record XFExt ($087D, §2.4.355), que pode anexar um RGB completo ou uma cor de tema a um formato de célula, mas o Excel 97–2003 e qualquer parser que lê apenas o record XF continuam não vendo nada além do índice
O HotXLS expõe a paleta por meio de um ColorIndex público de 1 a 56, que é o índice físico menos 7. Três esquemas de numeração estão em jogo, e o ResolveIndexedColor os mantém separados por meio do TXLSIndexedColorSpace: xicsPublicColorIndex para os valores de API 1..56, xicsBiffIcv para os índices crus do disco e xicsOoxmlIndexed, em que 64 e 65 significam primeiro plano e fundo do sistema. Índices BIFF crus também são checados contra o papel que você passar como TXLSIndexedColorRole, porque uma fonte, um preenchimento e um gráfico aceitam conjuntos de tokens diferentes; $51 é legal para uma fonte e inválido para um preenchimento
O resultado Boolean do ResolveIndexedColor é onde quem chama tropeça. Ele devolve True apenas quando obteve um valor ARGB concreto, e a sobrecarga curta nunca pergunta ao sistema operacional pelas cores do desktop. Um token de sistema ou automático, portanto, volta False com Res.Kind definido como xickSystem ou xickAutomatic, e código que lê False como "sem cor" descarta silenciosamente o significado do token. A única exceção é $4F, a cor neutra de gráfico, que resolve para preto sem ajuda. Para valores RGB reais nos outros tokens, passe um método TXLSTryResolveSystemColor à sobrecarga longa e aplique a sua própria política para UI, export ou serviço headless:
type
TExportColors = class
function ResolveSystemColor(AColor: TXLSSystemColorRole;
ARole: TXLSIndexedColorRole; out AArgb: LongWord): Boolean;
end;
function TExportColors.ResolveSystemColor(AColor: TXLSSystemColorRole;
ARole: TXLSIndexedColorRole; out AArgb: LongWord): Boolean;
begin
// Export headless: cores fixas em vez do tema atual do desktop
Result := True;
case AColor of
xscrWindowText, xscrChartText, xscrToolTipText:
AArgb := $FF000000;
xscrWindowBackground, xscrChartBackground:
AArgb := $FFFFFFFF;
else
Result := False;
end;
end;
var
Colors: TExportColors; // criado uma vez pelo seu código de export
Res: TXLSIndexedColorResolution;
begin
// $41 é o token de fundo do sistema, válido para um preenchimento
if Workbook.ResolveIndexedColor($41, xicsBiffIcv, xicrFillBackground,
Colors.ResolveSystemColor, Res) then
PaintBackground(Res.ARGB) // sua própria rotina de desenho
else if Res.Kind = xickInvalid then
RejectToken(Res.RawIndex); // seu próprio tratamento de erro
end;
Por que a distância RGB escolhe a entrada errada da paleta?
Os valores de canal sRGB são codificados com gamma, então passos iguais nos números não são passos iguais no que uma pessoa vê, e o erro é maior nos tons escuros e saturados que os templates corporativos adoram. Em RGB, o $000033 está a 51 do preto e a 77 do navy, então o preto vence. Depois que cada canal é linearizado e a cor é projetada no OKLab, as distâncias ao quadrado são cerca de 0.0312 até o preto e 0.0235 até o navy, e o navy vence. O casamento é o ColorIndex 11 no slot físico 18, e os motores Classic e XLSX dão a mesma resposta
A paleta padrão acrescenta uma complicação. O navy aparece duas vezes, no ColorIndex 11 e de novo no 25, então o $000033 está exatamente à mesma distância dos dois. Um casador que varre em outra ordem, ou usa menor-ou-igual, escolhe 25 em uma execução e 11 em outro build, e dois arquivos gerados param de dar diff limpo. O HotXLS varre do slot 8 para cima com comparação estrita de menor, então um empate sempre vai para o índice menor
OKLab é uma troca deliberada. O CIEDE2000 ranquearia alguns pares de matiz com mais precisão, mas suas correções por partes o tornam lento e mais difícil de raciocinar dentro de um loop iterativo. O OKLab custa uma linearização, dois produtos de matrizes pequenas e três raízes cúbicas por cor, depois do que uma distância simples ao quadrado é um substituto bom o bastante para diferença percebida. Para uma varredura de paleta de 56 entradas que roda a cada atribuição de cor e dentro de uma passada de clustering, a métrica mais barata e suave é a escolha certa
O que o NearestIndexedColor devolve?
O NearestIndexedColor recebe um valor ARGB e preenche um record TXLSNearestIndexedColorMatch sem mudar a pasta de trabalho. Cada pasta de trabalho mantém as 56 cores da paleta junto com suas coordenadas OKLab e um contador de geração da paleta, então uma consulta custa uma conversão de entrada e uma varredura fixa. Redefinir a paleta reconstrói esse cache, mudar uma única entrada atualiza só esse slot, e uma consulta contra um cache fora de sincronia com a geração atual devolve False em vez de chutar
O alpha segue uma regra estreita. Um byte alpha zero é tratado como opaco, o que combina com como a maioria do código Delphi passa valores RGB puros, e $FF é opaco por definição. Qualquer coisa no meio é rejeitada: a função devolve False com ColorIndex 0 e PaletteSlot -1, já que uma entrada de paleta não tem onde guardar transparência
var
Match: TXLSNearestIndexedColorMatch;
begin
if Workbook.NearestIndexedColor($FF000033, Match) then
begin
// Paleta padrão: Match.ColorIndex = 11, Match.PaletteSlot = 18,
// Match.ARGB = $FF000080
if not Match.ExactMatch then
LogApproximation(Match.InputARGB, Match.ARGB, // seu próprio logger
Match.DistanceSquared);
end;
end;
O mesmo casador fica atrás dos setters de RGB. Atribuir Interior.Color, Font.Color ou Border.Color em uma pasta Classic mantém a cor exata para o record XFExt e escolhe, com essa varredura, o índice guardado no record XF, e os writers de preenchimento e borda resolvem RGB e cores de tema para um índice do mesmo jeito. O que o NearestIndexedColor reporta para uma cor é o slot que um leitor apenas por índice mostrará
Encaixando uma pasta true color em 56 slots
Casar contra a paleta padrão serve para um punhado de cores. Um template de marca com uma dúzia de azuis quase idênticos precisa de mais: a própria paleta tem que mudar. O BuildBiffPalettePlan calcula uma proposta completa para todos os 56 slots e deixa a pasta de trabalho intocada, então você pode registrá-la, compará-la com um limite ou descartá-la
O planner começa com o ScanIndexedColorUsage. Todo slot referenciado por índice a partir de uma fonte, preenchimento, borda, formatação condicional, shape, comentário, fonte de gráfico ou linha de grade da planilha fica travado, porque mudar essa entrada recoloria de uma vez todo consumidor do índice. Os alvos são os RGB diretos e as cores de tema resolvidas vindas de fontes, preenchimentos, pontos de gradiente, bordas, estilos de formatação condicional, data bars e color scales. Cada alvo é ponderado pelo maior entre sua contagem de referências e sua contagem de definições, e uma formatação condicional conta toda célula que seus intervalos cobrem, então um preenchimento aplicado por uma coluna inteira pesa mais que uma cor usada em um comentário. O posicionamento então segue uma ordem fixa:
- Slots travados mantêm sua cor atual incondicionalmente
- Um alvo que já existe na paleta mantém seu slot correspondente mais baixo, e esse slot vira fixo
- Se os alvos distintos restantes cabem nos slots livres, cada um ganha um slot exato, atribuído em ordem crescente de ARGB
- Caso contrário,
Quantizedé definido, cada slot livre é semeado com o alvo cuja distância até o centro existente mais próximo, multiplicada pelo seu peso, é a maior, e até 16 rodadas de k-means ponderado em OKLab movem apenas os centros livres até as atribuições pararem de mudar
O caminho de overflow merece uma leitura de olhos abertos. O k-means aqui é uma otimização local limitada, e um slot livre acaba guardando um centro de cluster convertido de volta para sRGB com clamping, que pode ser uma cor que nenhuma célula usou verbatim. O que o planner entrega de verdade é repetibilidade: a mesma pasta de trabalho sempre produz o mesmo plano. O plano também reporta até onde teve que dobrar por meio de WeightedError, MaxDistanceSquared, ExactTargetWeight e TotalTargetWeight, o que basta para um job em lote se recusar a salvar quando a aproximação fica grosseira demais para um guia de marca. O array Targets mostra, por cor de entrada, o slot em que ela cai e sua distância ao quadrado
const
MaxAcceptedError = 0.002; // seu limite, em unidades OKLab ao quadrado
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); // seu próprio logger
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
Quando o ApplyBiffPalettePlan recusa um plano?
O ApplyBiffPalettePlan verifica 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, que as contagens de travados e mudados fechem, e que todo alvo seja opaco
Qualquer mudança real de paleta entre o planejamento e a aplicação torna o plano obsoleto, e isso inclui uma aplicação anterior bem-sucedida do mesmo plano. Planos são de uso único na prática: monte, inspecione, aplique, e monte de novo se precisar de outro. Um plano válido sem slots mudados tem sucesso sem avançar a geração. Uma mudança real a avança uma vez e atualiza o casador uma vez. No motor Classic as novas cores vão direto para a paleta; no motor XLSX elas viram cores indexadas personalizadas, que o HasCustomIndexedColor e o CustomIndexedColorCount reportam e que o writer XLSX emite como um bloco indexedColors
Optando por saves XLS e conversão de XLSX para XLS
A propriedade BiffPaletteSavePolicy tem xbpsPreserve como padrão, então a paleta de uma pasta de trabalho só muda quando você pede. Definir xbpsOptimizeTrueColors faz uma pasta Classic montar e aplicar um plano novo dentro do SaveAs, e apenas quando o formato de destino é xlExcel97; BIFF5, CSV, HTML, PDF, XLSX e os outros writers ignoram a configuração. Depois de um save bem-sucedido, a paleta otimizada fica na pasta de trabalho, então consultas e saves posteriores veem o mesmo mapeamento. Se o save falha, é cancelado ou levanta erro, as 56 cores originais e a geração original são restauradas
Para relatórios gerados, ligue a política de otimização. Essas pastas de trabalho são construídas a partir de RGB e cores de tema, ninguém ajustou a paleta delas à mão, e um leitor apenas por índice as mostra bem mais fiéis ao original com ela. Mantenha o xbpsPreserve para pastas de trabalho cuja paleta faz parte do contrato, como um template que código a jusante edita pelo ColorIndex, em que um slot mudar de cor silenciosamente quebraria esse código
Origens XLSX seguem outro caminho. O SaveXLSXWorkbookAsXLS em lxXlsxExport monta um único plano a partir da TXLSXWorkbook carregada e o grava na paleta de destino antes de converter qualquer estilo, então toda célula convertida é casada contra a paleta final. A demo de workbench de auditoria e conversão de pastas de trabalho roda exatamente esse caminho. Cores de tema passam pelo mesmo planner depois que o tint delas é resolvido para RGB; preenchimentos de gráfico que devem continuar referências vivas de tema são um caso à parte, coberto no artigo sobre cores de tema GelFrame em preenchimentos de gráfico, em que o XLS binário guarda um índice de esquema em vez de uma cor achatada
// Pasta Classic: opt in, vale apenas para saves BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // seu próprio handler; 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
if SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls') <> 1 then
HandleSaveFailure;
finally
XWorkbook.Free;
end;
As chamadas de paleta (ResolveIndexedColor, NearestIndexedColor, ScanIndexedColorUsage, BuildBiffPalettePlan e ApplyBiffPalettePlan) têm as mesmas assinaturas em IXLSWorkbook e TXLSXWorkbook, tanto do Delphi quanto do C++Builder. O build trial na página do HotXLS Delphi Excel component basta para rodar um plano contra o seu template mais colorido e ler o MaxDistanceSquared antes de qualquer coisa ser gravada