Artículo técnico

Paleta BIFF8 de 56 colores: mapeo OKLab en HotXLS

HotXLS mapea colores RGB y de tema arbitrarios sobre la paleta BIFF8 de 56 ranuras en dos capas: NearestIndexedColor encuentra en el espacio OKLab la entrada de paleta existente perceptualmente más cercana, y BuildBiffPalettePlan con ApplyBiffPalettePlan reescribe las ranuras libres para que un libro en color verdadero sobreviva a un guardado como XLS clásico. El detonante siempre es el mismo ticket de soporte. Alguien arma un reporte en XLSX con encabezados azul marino corporativo y un acento verde azulado suave, lo guarda como .xls para un consumidor heredado, y los encabezados vuelven negros puros mientras el verde azulado termina en un turquesa estridente. Nada se cayó y no saltó ninguna advertencia. El modelo de color del formato viejo simplemente no puede contener lo que el nuevo describió, y la librería tuvo que elegir algo

¿Por qué un archivo XLS solo puede contener 56 colores?

Porque un formato de celda BIFF8 nunca guarda un valor RGB: fuentes, rellenos y bordes llevan un índice de color, y el registro Palette global del libro ($0092, [MS-XLS] §2.4.188) aporta exactamente 56 entradas RGB opacas para los índices 8 al 63. Los índices 0 al 7 son copias fijas de los ocho colores básicos, y los valores por encima de 63 ni siquiera son colores sino tokens como primer plano del sistema, fondo del sistema y texto de gráfico. HotXLS expone la paleta a través de un ColorIndex público de 1 a 56, que es el índice físico menos 7, y ResolveIndexedColor mantiene separados los tres esquemas de numeración mediante TXLSIndexedColorSpace: xicsPublicColorIndex para los valores 1..56 de la API, xicsBiffIcv para los índices crudos en disco, que se validan contra el subconjunto IcvFont, IcvXF o IcvChart según el rol que usted pase, y xicsOoxmlIndexed, donde 64 y 65 significan primer plano y fondo del sistema

HotXLS mantiene separados los tres esquemas de color indexado mediante TXLSIndexedColorSpace: valores icv BIFF crudos, 0 a 7 fijos a los ocho colores básicos, las 56 ranuras 8 a 63 del registro Palette $0092, tokens por encima de 63 como primer plano del sistema, ColorIndex público 1 a 56 con desfase de menos 7, y xicsOoxmlIndexed donde 64 y 65 significan primer plano y fondo del sistema
El mismo índice de color significa números distintos en cada esquema, así que HotXLS enruta cada valor por ResolveIndexedColor en lugar de dejar que un token BIFF crudo se haga pasar por un ColorIndex público
var
  Res: TXLSIndexedColorResolution;
begin
  // $40 es un token icv de BIFF, no una ranura de paleta
  Workbook.ResolveIndexedColor($40, xicsBiffIcv, Res);
  case Res.Kind of
    xickPalette:   UseArgb(Res.ARGB);   // ranura de paleta, si se resolvió
    xickAutomatic,
    xickSystem:    UseSystemColor(Res.SystemColorRole);
    xickInvalid:   RejectToken(Res.RawIndex);
  end;
end;

Observe que el ejemplo conmuta sobre Res.Kind e ignora el valor booleano de retorno. ResolveIndexedColor devuelve True solo cuando obtuvo un ARGB concreto, y la sobrecarga corta nunca lee el escritorio de Windows, así que un token automático o de sistema legítimamente vuelve False aun clasificado como xickSystem. HotXLS se topó con esto en su propio serializador de libros: código que trata False como «sin color» descarta silenciosamente el significado Automatic y System del token. Si necesita valores RGB reales para esos tokens, llame a la sobrecarga larga y suministre un callback TXLSTryResolveSystemColor que aplique su propia política de UI, exportación o ejecución headless

¿Por qué HotXLS compara colores en OKLab y no en RGB?

Porque los valores de canal sRGB están codificados con gamma, así que la distancia euclidiana en RGB no sigue lo que una persona ve, y el error es peor justo en los tonos oscuros y saturados que adoran las paletas corporativas. Tome el azul oscuro $000033. En RGB la distancia al negro es 51 y la distancia a la entrada azul marino por defecto $000080 es 77, así que un comparador RGB pinta su encabezado de negro con toda confianza. En OKLab las distancias al cuadrado son unos 0.0312 al negro y 0.0235 al azul marino, y HotXLS elige azul marino, ColorIndex 11 en la ranura física 18; ese caso exacto está fijado en la suite de pruebas para el motor Classic y para el XLSX. La conversión dentro de ArgbToOklab linealiza cada canal sRGB, aplica la matriz LMS de OKLab, saca raíces cúbicas y proyecta sobre L, a y b, tras lo cual una distancia euclidiana al cuadrado sin más es una aproximación razonable de la diferencia percibida. OKLab no es CIEDE2000 ni pretende serlo, pero no tiene correcciones de matiz por tramos, cuesta un puñado de multiplicaciones por color, y es lo bastante estable para gobernar un loop de clustering, que es donde de verdad gana su lugar

Cómo HotXLS lleva el azul oscuro $000033 a la paleta: la distancia euclidiana en RGB con gamma mide 51 al negro y 77 al azul marino y pintaría el encabezado de negro, mientras que las distancias al cuadrado de ArgbToOklab, 0.0312 y 0.0235, permiten a NearestIndexedColor elegir azul marino, ColorIndex 11 en la ranura física 18
Los valores de canal con gamma hacen de la distancia RGB un mal sustituto de lo que una persona ve, así que HotXLS convierte una sola vez a OKLab y deja que una comparación euclidiana al cuadrado simple gobierne el barrido de la paleta

¿Qué garantiza NearestIndexedColor?

NearestIndexedColor garantiza una respuesta determinista y de solo lectura: una conversión de la entrada, un barrido fijo sobre 56 entradas en caché y el índice público más bajo cuando dos entradas empatan en cercanía. Cada libro cachea el ARGB normalizado y las coordenadas OKLab de las 56 ranuras físicas junto con un contador de generación de paleta. Un reset de la paleta reconstruye la caché, un cambio de una sola ranura actualiza solo esa ranura, y una consulta contra una generación vencida devuelve False en lugar de adivinar. El barrido usa una comparación estricta de menor que arrancando en la ranura 8, razón por la cual una paleta que contenga dos veces el mismo color siempre responde con el índice más bajo; eso importa cuando usted compara (diff) dos archivos generados y espera salida idéntica byte a byte. El alfa de entrada sigue un contrato estrecho: un byte alfa cero se trata como opaco, y un valor parcialmente transparente se rechaza con ColorIndex 0 y PaletteSlot -1, ya que las entradas de paleta no tienen alfa. Los escritores de relleno y borde del motor Classic convierten colores RGB y de tema a un índice con la misma rutina de igualación OKLab al momento de guardar, así que la API y el archivo almacenado concuerdan en qué ranura aterriza cada color

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;

¿Cómo mete BuildBiffPalettePlan colores verdaderos en 56 ranuras?

BuildBiffPalettePlan computa una propuesta completa para las 56 ranuras sin tocar el libro, de modo que usted puede inspeccionarla, registrarla o descartarla. El planificador primero llama a ScanIndexedColorUsage: toda ranura que una fuente, un relleno, un borde, un formato condicional, una forma, un comentario o una línea de cuadrícula de la hoja referencie por índice queda bloqueada, porque cambiar una entrada de paleta recolorea de golpe a todos los consumidores de ese índice. Los objetivos son los colores RGB directos y los colores de tema resueltos de fuentes, rellenos, bordes, estilos diferenciales, barras de datos y escalas de color. Cada objetivo se pondera por el mayor entre su conteo de referencias renderizadas y su conteo de definiciones, y un formato condicional cuenta las celdas que cubren sus rangos, así que un color pintado sobre toda una columna pesa más que uno usado en una sola nota. La colocación luego procede en un orden fijo:

  • Las ranuras bloqueadas conservan su color de origen incondicionalmente
  • Un objetivo que ya existe en la paleta se retiene en su ranura coincidente más baja y esa ranura queda fija
  • Si los objetivos únicos restantes caben en las ranuras libres, cada uno recibe una ranura exacta, asignada en orden ascendente de ARGB
  • De lo contrario se activa Quantized, cada ranura libre se siembra con el objetivo cuya distancia a su centro existente más cercano, multiplicada por su peso, sea la mayor, y hasta 16 rondas de k-means ponderado por frecuencia en OKLab mueven solo los centros libres hasta que las asignaciones dejan de cambiar

Sea honesto consigo mismo sobre lo que entrega el camino de desborde. El clustering es una optimización local acotada, no un óptimo global, y una ranura libre termina conteniendo un centroide convertido de vuelta a sRGB con recorte de rango, que puede ser un color que ninguna celda usó textualmente. Lo que sí obtiene es repetibilidad: el mismo libro siempre produce el mismo plan, y el plan reporta su propio daño mediante WeightedError, MaxDistanceSquared, ExactTargetWeight y TotalTargetWeight, de modo que un trabajo por lotes puede negarse a guardar cuando la aproximación queda demasiado gruesa para una guía de marca

El pipeline de paleta de HotXLS para un libro en color verdadero: ScanIndexedColorUsage bloquea toda ranura referenciada por fuente, relleno, borde, formato condicional, forma, comentario o línea de cuadrícula, BuildBiffPalettePlan coloca colores exactos en orden ascendente de ARGB o corre hasta 16 rondas de k-means ponderado por frecuencia en OKLab, y ApplyBiffPalettePlan valida la generación y el hash FNV-1a antes de escribir
La planificación es de solo lectura y repetible, el plan reporta su propio daño mediante WeightedError y MaxDistanceSquared, y un plan vencido se rechaza con la paleta intacta porque los planes son en la práctica de un solo uso
var
  Plan: TXLSBiffPalettePlan;
  I: Integer;
begin
  Plan := Workbook.BuildBiffPalettePlan;   // solo lectura
  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;

¿Cómo rechaza ApplyBiffPalettePlan un plan vencido?

ApplyBiffPalettePlan valida el plan completo antes de escribir una sola ranura, y devuelve False con la paleta intacta si algo no coincide con el libro actual. El plan lleva SourcePaletteGeneration y SourcePaletteHash, un hash FNV-1a de 64 bits sobre los 56 colores de origen; la validación además revisa cada índice público y físico, cada color de origen, que ninguna ranura bloqueada esté marcada como cambiada, los conteos de ranuras bloqueadas y cambiadas, y que cada objetivo sea opaco. Cualquier cambio efectivo de paleta que ocurra en medio, incluida una aplicación anterior con éxito del mismo plan, deja el plan vencido, así que los planes son en la práctica de un solo uso. Un plan válido sin ranuras cambiadas tiene éxito sin avanzar la generación, y un cambio real incrementa la generación una vez y reconstruye el comparador OKLab una vez, en el motor Classic reescribiendo el arreglo fijo de paleta y en el motor XLSX sustituyendo una lista de override de colores indexados preparada de antemano

Activación para guardados BIFF8 y conversión de XLSX a XLS

La propiedad BiffPaletteSavePolicy viene por defecto en xbpsPreserve, así que actualizar HotXLS nunca reescribe la paleta de nadie a sus espaldas. Ponerla en xbpsOptimizeTrueColors hace que un libro Classic construya y aplique un plan fresco dentro de SaveAs, pero solo cuando el formato destino es xlExcel97; los escritores BIFF5, CSV, HTML, PDF, XLSX y demás ignoran el ajuste. Tras un guardado exitoso, la paleta optimizada se queda en el modelo del libro, de modo que consultas y guardados posteriores ven el mismo mapeo. Si el guardado falla o se cancela, se restauran los 56 colores originales y la generación original. Para fuentes XLSX, SaveXLSXWorkbookAsXLS en lxXlsxExport construye un plan desde el libro cargado y lo escribe en la paleta destino antes de convertir cualquier estilo, que es el puente determinista que ejercita la demo del workbench de auditoría y conversión de libros. Los colores de tema pasan por el mismo planificador después de que su tint se resuelve a RGB; si prefiere mantener los temas vivos en los rellenos de gráficos, el artículo sobre rellenos de gráficos con colores de tema GelFrame cubre cómo el XLS binario guarda un índice de esquema en lugar de un color aplanado

// Libro Classic: opt-in, solo BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
  HandleSaveFailure;   // la paleta ya fue restaurada

// Modelo XLSX a BIFF8 con un único plan de paleta determinista
XWorkbook := TXLSXWorkbook.Create;
try
  if XWorkbook.Open('report.xlsx') = 1 then
    SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls');
finally
  XWorkbook.Free;
end;

Las APIs de paleta de HotXLS funcionan igual sobre IXLSWorkbook y TXLSXWorkbook, desde Delphi y C++Builder por igual. Descargue la versión de prueba y pásela por su hoja de cálculo más colorida desde la página del componente Excel HotXLS para Delphi