O HotXLS mapeia cores RGB e de tema arbitrárias na paleta de 56 cores do BIFF8 em duas camadas: o NearestIndexedColor encontra em espaço OKLab a entrada já existente da paleta perceptualmente mais próxima, e o BuildBiffPalettePlan com o ApplyBiffPalettePlan reescreve as entradas livres da paleta para que um livro de trabalho a cores verdadeiras sobreviva a uma gravação em XLS clássico. O gatilho é sempre o mesmo ticket de suporte. Alguém constrói um relatório em XLSX com cabeçalhos azul-marinho institucionais e um realce turquesa suave, grava-o como .xls para um consumidor antigo, e os cabeçalhos voltam pretos puros enquanto o turquesa se transforma num turquesa berrante. Nada rebentou e nenhum aviso disparou. O modelo de cores do formato antigo simplesmente não consegue guardar o que o novo descreveu, e a biblioteca tinha de escolher alguma coisa
Porque é que um ficheiro XLS só consegue guardar 56 cores?
Porque o formato de célula do BIFF8 nunca guarda um valor RGB: os tipos de letra, os preenchimentos e os limites transportam um índice de cor, e o registo Palette global do livro ($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 sequer são cores, mas tokens como primeiro plano do sistema, fundo do sistema e texto de gráfico. O HotXLS expõe a paleta através de 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 através do TXLSIndexedColorSpace: xicsPublicColorIndex para os valores de API 1..56, xicsBiffIcv para os índices brutos em disco, que são validados contra o subconjunto IcvFont, IcvXF ou IcvChart do papel que indicar, e xicsOoxmlIndexed, onde 64 e 65 significam primeiro plano e fundo do sistema
var
Res: TXLSIndexedColorResolution;
begin
// $40 é um token icv do BIFF, não uma entrada da paleta
Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
case Res.Kind of
xickPalette: UseArgb(Res.ARGB); // entrada da paleta, se resolvida
xickAutomatic,
xickSystem: UseSystemColor(Res.SystemColorRole);
xickInvalid: RejectToken(Res.RawIndex);
end;
end;
Note que o exemplo comuta sobre o Res.Kind e ignora o valor de retorno Booleano. O ResolveIndexedColor só devolve True quando obteve um ARGB concreto, e a sobrecarga curta nunca lê o ambiente de trabalho do Windows, pelo que um token automático ou de sistema legitimamente volta False continuando classificado como xickSystem. O HotXLS topou com isto no seu próprio serializador de livros: código que trata False como «sem cor» deita fora silenciosamente o significado Automatic e System do token. Se precisar de valores RGB reais para esses tokens, chame a sobrecarga longa e forneça um callback TXLSTryResolveSystemColor que aplique a sua própria política de UI, de exportação ou headless
Porque é que o HotXLS faz corresponder cores em OKLab em vez de RGB?
Porque os valores dos canais sRGB estão codificados em gama, pelo que a distância euclidiana em RGB não acompanha o que uma pessoa vê, e o erro é pior precisamente nos tons escuros e saturados de que as paletas institucionais gostam. Pegue no azul-escuro $000033. Em RGB a distância ao preto é 51 e a distância à entrada azul-marinho predefinida $000080 é 77, pelo que um comparador em RGB pinta-lhe o cabeçalho de preto com toda a confiança. Em OKLab as distâncias ao quadrado são cerca de 0,0312 até ao preto e 0,0235 até ao azul-marinho, e o HotXLS escolhe o azul-marinho, ColorIndex 11 na entrada física 18; esse caso exato está afixado na suite de testes tanto no motor Classic como no 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, após o que uma distância euclidiana ao quadrado simples é uma aproximação razoável da diferença percebida. O OKLab não é CIEDE2000 e não finge que é, mas não tem correções de matiz por intervalos, custa um punhado de multiplicações por cor e é estável o suficiente para comandar um ciclo de clustering, que é onde ganha mesmo o seu lugar
O que garante o NearestIndexedColor?
O NearestIndexedColor garante uma resposta determinística e só de leitura: uma conversão da entrada, um varrimento fixo sobre 56 entradas em cache, e o índice público mais baixo sempre que duas entradas estejam igualmente próximas. Cada livro mantém em cache o ARGB normalizado e as coordenadas OKLab das 56 entradas físicas juntamente com um contador de geração da paleta. Um reset da paleta reconstrói a cache, uma alteração de uma só entrada atualiza apenas essa entrada, e uma consulta contra uma geração obsoleta devolve False em vez de adivinhar. O varrimento usa uma comparação estrita de menor que a começar na entrada 8, razão pela qual uma paleta que contenha a mesma cor duas vezes responde sempre com o índice mais baixo; isso importa quando faz diff entre dois ficheiros gerados e espera saída byte a byte idêntica. O alfa de entrada segue um contrato apertado: um byte de alfa zero é tratado como opaco, e um valor parcialmente transparente é recusado com ColorIndex 0 e PaletteSlot -1, já que as entradas da paleta não têm alfa. Os escritores de preenchimento e de limites do motor Classic convertem cores RGB e de tema num índice com a mesma rotina de correspondência OKLab no momento de gravar, pelo que a API e o ficheiro guardado concordam sobre a entrada em que uma cor acaba por cair
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 é que o BuildBiffPalettePlan encaixa cores verdadeiras em 56 entradas?
O BuildBiffPalettePlan calcula uma proposta completa para as 56 entradas sem tocar no livro, pelo que a pode inspecionar, registar ou descartar. O planeador começa por chamar o ScanIndexedColorUsage: qualquer entrada que um tipo de letra, um preenchimento, um limite, um formato condicional, uma forma, um comentário ou uma linha de grelha da folha referencie por índice fica bloqueada, porque alterar uma entrada da paleta recolore de uma vez todos os consumidores desse índice. Os alvos são as cores RGB diretas e as cores de tema resolvidas de tipos de letra, preenchimentos, limites, estilos diferenciais, barras de dados e escalas de cores. Cada alvo é ponderado pelo maior entre a sua contagem de referências renderizadas e a sua contagem de definições, e um formato condicional conta as células que os seus intervalos cobrem, pelo que uma cor pintada ao longo de uma coluna inteira pesa mais do que uma usada numa única nota. A colocação prossegue depois numa ordem fixa:
- As entradas bloqueadas conservam a sua cor de origem incondicionalmente
- Um alvo que já exista na paleta é retido na sua entrada correspondente mais baixa e essa entrada fica fixa
- Se os alvos únicos restantes couberem nas entradas livres, cada um recebe uma entrada exata, atribuída por ordem ARGB crescente
- Caso contrário, o
Quantizedé ativado, cada entrada livre é semeada com o alvo cuja distância ao centro existente mais próximo, multiplicada pelo seu peso, é maior, e até 16 rondas de k-means ponderado por frequência em OKLab movem apenas os centros livres até as atribuições pararem de mudar
Seja honesto consigo próprio quanto ao que o caminho de transbordo entrega. O clustering é uma otimização local limitada, não um ótimo global, e uma entrada livre acaba por conter um centróide convertido de volta para sRGB com clamping, que pode ser uma cor que nenhuma célula usou literalmente. O que obtém é repetibilidade: o mesmo livro produz sempre o mesmo plano, e o plano reporta os seus próprios danos através de WeightedError, MaxDistanceSquared, ExactTargetWeight e TotalTargetWeight, pelo que um job em lote pode recusar gravar quando a aproximação fica demasiado grosseira para uma guia de marca
var
Plan: TXLSBiffPalettePlan;
I: Integer;
begin
Plan := Workbook.BuildBiffPalettePlan; // só de 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 é que o ApplyBiffPalettePlan recusa um plano obsoleto?
O ApplyBiffPalettePlan valida o plano inteiro antes de escrever uma única entrada, e devolve False com a paleta intacta se algo discordar do livro atual. O plano transporta SourcePaletteGeneration e SourcePaletteHash, um hash FNV-1a de 64 bits sobre as 56 cores de origem; a validação também reverifica cada índice público e físico, cada cor de origem, que nenhuma entrada bloqueada esteja marcada como alterada, as contagens de bloqueadas e de alteradas, e que todos os alvos são opacos. Qualquer alteração efetiva da paleta entretanto, incluindo uma aplicação anterior bem-sucedida do mesmo plano, torna o plano obsoleto, pelo que os planos são na prática de utilização única. Um plano válido sem entradas alteradas tem êxito sem avançar a geração, e uma alteração real avança a geração uma vez e reconstrói o comparador OKLab uma vez, no motor Classic reescrevendo a matriz fixa da paleta e no motor XLSX trocando por uma lista de substituição de cores indexadas preparada
Ativar para gravações BIFF8 e conversão de XLSX para XLS
A propriedade BiffPaletteSavePolicy assume por predefinição o valor xbpsPreserve, pelo que atualizar o HotXLS nunca reescreve a paleta de ninguém às escondidas. Defini-la como xbpsOptimizeTrueColors faz com que um livro Classic construa e aplique um plano fresco dentro do SaveAs, mas apenas quando o formato de destino é xlExcel97; os escritores BIFF5, CSV, HTML, PDF, XLSX e os restantes ignoram a definição. Depois de uma gravação bem-sucedida, a paleta otimizada fica no modelo do livro, pelo que consultas e gravações posteriores veem o mesmo mapeamento. Se a gravação falhar ou for cancelada, as 56 cores originais e a geração original são repostas. Para origens XLSX, o SaveXLSXWorkbookAsXLS em lxXlsxExport constrói um único plano a partir do livro carregado e escreve-o na paleta de destino antes de qualquer estilo ser convertido, que é a ponte determinística que a demonstração da bancada de auditoria e conversão de livros exercita. As cores de tema passam pelo mesmo planeador depois do seu tint ser resolvido para RGB; se preferir manter os temas vivos nos preenchimentos de gráficos, o artigo sobre preenchimentos de gráficos com cores de tema GelFrame explica como o XLS binário guarda um índice de esquema em vez de uma cor achatada
// Livro Classic: ativar à mão, só BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // paleta já reposta
// 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 da mesma forma no IXLSWorkbook e no TXLSXWorkbook, a partir do Delphi e do C++Builder indistintamente. Transfira a versão de avaliação e aponte-a para a sua folha de cálculo mais colorida a partir da página do componente Excel HotXLS para Delphi