El azul oscuro $000033 queda a 51 unidades del negro y a 77 unidades de la entrada clásica de azul marino $000080 cuando la distancia se mide como distancia euclidiana simple sobre los valores de canal RGB. Un conversor que escribe archivos .xls clásicos con igualación por RGB más cercano pinta por tanto una fila de encabezado azul oscuro de negro, y negro es lo que muestra todo lector que va solo por el índice de paleta. El formato no le deja margen al escritor: un formato de celda guarda un índice de paleta, y la paleta tiene 56 entradas. Poner el encabezado bien requiere dos cosas, una medida de distancia que siga lo que la gente ve, y una manera de reescribir la paleta misma cuando un libro usa más colores distintos de los que la paleta puede contener
De dónde sale el límite de 56 colores
Un record BIFF8 de fuente, relleno o borde nunca lleva un valor RGB. Lleva 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. Los valores por encima de 63 no son colores sino tokens: $40 es el primer plano del sistema, $41 el fondo del sistema, $4D a $4F son texto de gráfico, fondo de gráfico y el color neutro de gráfico, y $7FFF significa automático. Excel 2007 agregó el record XFExt ($087D, §2.4.355), que puede adjuntar un RGB completo o un color de tema a un formato de celda, pero Excel 97–2003 y cualquier parser que lea solo el record XF no ven nada excepto el índice
HotXLS expone la paleta a través de un ColorIndex público de 1 a 56, que es el índice físico menos 7. Tres esquemas de numeración están en juego, y ResolveIndexedColor los mantiene separados mediante TXLSIndexedColorSpace: xicsPublicColorIndex para los valores 1..56 de la API, xicsBiffIcv para los índices crudos en disco, y xicsOoxmlIndexed, donde 64 y 65 significan primer plano y fondo del sistema. Los índices BIFF crudos también se validan contra el rol que usted pase como TXLSIndexedColorRole, porque una fuente, un relleno y un gráfico aceptan conjuntos de tokens distintos; $51 es legal para una fuente e inválido para un relleno
El resultado Boolean de ResolveIndexedColor es donde los llamadores tropiezan. Devuelve True solo cuando obtuvo un valor ARGB concreto, y el overload corto nunca le pide al sistema operativo los colores de escritorio. Un token de sistema o automático por tanto vuelve False con Res.Kind en xickSystem o xickAutomatic, y el código que lee False como “sin color” descarta silenciosamente el significado del token. La única excepción es $4F, el color neutro de gráfico, que se resuelve a negro sin ayuda. Para valores RGB reales en los demás tokens, pase un método TXLSTryResolveSystemColor al overload largo y aplique su propia política para la UI, una exportación o un servicio 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: colores fijos en lugar del tema actual del escritorio
Result := True;
case AColor of
xscrWindowText, xscrChartText, xscrToolTipText:
AArgb := $FF000000;
xscrWindowBackground, xscrChartBackground:
AArgb := $FFFFFFFF;
else
Result := False;
end;
end;
var
Colors: TExportColors; // creado una vez por su código de exportación
Res: TXLSIndexedColorResolution;
begin
// $41 es el token de fondo del sistema, válido para un fill
if Workbook.ResolveIndexedColor($41, xicsBiffIcv, xicrFillBackground,
Colors.ResolveSystemColor, Res) then
PaintBackground(Res.ARGB) // su propia rutina de dibujo
else if Res.Kind = xickInvalid then
RejectToken(Res.RawIndex); // su propio manejo de errores
end;
¿Por qué la distancia RGB elige la entrada de paleta equivocada?
Los valores de canal sRGB están codificados con gamma, así que pasos iguales en los números no son pasos iguales en lo que una persona ve, y el error es mayor justo en los tonos oscuros y saturados que adoran las plantillas corporativas. En RGB, $000033 está a 51 del negro y a 77 del azul marino, así que gana el negro. Una vez que cada canal se linealiza y el color se proyecta a OKLab, las distancias al cuadrado son unos 0.0312 al negro y 0.0235 al azul marino, y gana el azul marino. El match es ColorIndex 11 en la ranura física 18, y los motores Classic y XLSX dan la misma respuesta
La paleta por defecto agrega una arruga. El azul marino aparece dos veces, en ColorIndex 11 y otra vez en 25, así que $000033 está exactamente igual de cerca de ambos. Un matcher que escanea en otro orden, o que usa menor o igual, elige 25 en una corrida y 11 en otro build, y dos archivos generados dejan de diff limpio. HotXLS escanea desde la ranura 8 hacia arriba con una comparación estricta de menor que, así que un empate siempre va al índice más bajo
OKLab es un trade deliberado. CIEDE2000 clasificaría algunos pares de matices con más precisión, pero sus correcciones por tramos lo hacen lento y más difícil de razonar dentro de un loop iterativo. OKLab cuesta una linealización, dos productos de matrices pequeños y tres raíces cúbicas por color, tras lo cual una distancia al cuadrado simple es un sustituto bastante bueno de la diferencia percibida. Para un barrido de paleta de 56 entradas que corre en cada asignación de color y dentro de una pasada de clustering, la métrica más barata y suave es la elección correcta
¿Qué devuelve NearestIndexedColor?
NearestIndexedColor toma un valor ARGB y llena un record TXLSNearestIndexedColorMatch sin cambiar el libro. Cada libro mantiene juntos los 56 colores de paleta con sus coordenadas OKLab y un contador de generación de paleta, así que una consulta cuesta una conversión de entrada y un barrido fijo. Resetear la paleta reconstruye esa caché, cambiar una sola entrada actualiza solo esa ranura, y una consulta contra una caché desalineada de la generación actual devuelve False en lugar de adivinar
El alfa sigue una regla estrecha. Un byte de alfa cero se trata como opaco, lo que coincide con cómo la mayoría del código Delphi pasa valores RGB simples, y $FF es opaco por definición. Todo lo intermedio se rechaza: la función devuelve False con ColorIndex 0 y PaletteSlot -1, ya que una entrada de paleta no tiene dónde guardar transparencia
var
Match: TXLSNearestIndexedColorMatch;
begin
if Workbook.NearestIndexedColor($FF000033, Match) then
begin
// Paleta por defecto: Match.ColorIndex = 11, Match.PaletteSlot = 18,
// Match.ARGB = $FF000080
if not Match.ExactMatch then
LogApproximation(Match.InputARGB, Match.ARGB, // su propio logger
Match.DistanceSquared);
end;
end;
El mismo matcher está detrás de los setters de RGB. Asignar Interior.Color, Font.Color o Border.Color en un libro Classic conserva el color exacto para el record XFExt y elige con este barrido el índice guardado en el record XF, y los escritores de relleno y borde resuelven colores RGB y de tema a un índice de la misma manera. Lo que NearestIndexedColor reporte para un color es la ranura que mostrará un lector de solo índices
Meter un libro en color verdadero en 56 ranuras
Igualar contra la paleta por defecto está bien para un puñado de colores. Una plantilla de marca con una docena de azules casi idénticos necesita más: la paleta misma tiene que cambiar. BuildBiffPalettePlan computa una propuesta completa para las 56 ranuras y deja el libro intacto, así que usted puede hacerle log, compararla contra un umbral o tirarla
El planificador arranca con ScanIndexedColorUsage. Toda ranura referenciada por índice desde una fuente, relleno, borde, formato condicional, forma, comentario, fuente de gráfico o línea de cuadrícula de la hoja queda bloqueada, porque cambiar esa entrada recolorearía de golpe a todos los consumidores del índice. Los objetivos son los RGB directos y los colores de tema resueltos de fuentes, rellenos, paradas de gradiente, bordes, estilos de formato condicional, barras de datos y escalas de color. Cada objetivo se pondera por el mayor entre su conteo de referencias y su conteo de definiciones, y un formato condicional cuenta cada celda que sus rangos cubren, así que un relleno pintado sobre toda una columna pesa más que un color usado en un comentario. La colocación luego sigue un orden fijo:
- Las ranuras bloqueadas conservan su color actual incondicionalmente
- Un objetivo que ya existe en la paleta conserva su ranura coincidente más baja, y esa ranura queda fija
- Si los objetivos distintos 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 en OKLab mueven solo los centros libres hasta que las asignaciones dejan de cambiar
El camino de desborde merece una lectura sin ilusiones. El k-means de aquí es una optimización local acotada, y una ranura libre termina conteniendo un centro de clúster convertido de vuelta a sRGB con recorte de rango, que puede ser un color que ninguna celda usó textualmente. Lo que el planificador sí entrega es repetibilidad: el mismo libro siempre produce el mismo plan. El plan también reporta cuánto tuvo que doblarse mediante WeightedError, MaxDistanceSquared, ExactTargetWeight y TotalTargetWeight, que es suficiente para que un trabajo por lotes se niegue a guardar cuando la aproximación queda demasiado gruesa para una guía de marca. El arreglo Targets muestra, por color de entrada, la ranura a la que mapea y su distancia al cuadrado
const
MaxAcceptedError = 0.002; // su umbral, en unidades OKLab al cuadrado
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); // su propio logger
if not Workbook.ApplyBiffPalettePlan(Plan) then
raise Exception.Create('The palette changed after planning');
end;
¿Cuándo rechaza ApplyBiffPalettePlan un plan?
ApplyBiffPalettePlan verifica 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 re-verifica cada índice público y físico, cada color de origen, que ninguna ranura bloqueada esté marcada como cambiada, que los conteos de bloqueadas y cambiadas sumen, y que cada objetivo sea opaco
Cualquier cambio real de paleta entre planear y aplicar deja el plan vencido, y eso incluye una aplicación anterior exitosa del mismo plan. Los planes son de un solo uso en la práctica: construya, inspeccione, aplique, y construya otra vez si necesita otro. Un plan válido sin ranuras cambiadas tiene éxito sin avanzar la generación. Un cambio real la avanza una vez y refresca el matcher una vez. En el motor Classic los colores nuevos van directo a la paleta; en el motor XLSX se vuelven colores indexados personalizados, que HasCustomIndexedColor y CustomIndexedColorCount reportan y que el escritor XLSX emite como un bloque indexedColors
Optar por guardados XLS y conversión de XLSX a XLS
La propiedad BiffPaletteSavePolicy viene por defecto en xbpsPreserve, así que la paleta de un libro solo cambia cuando usted lo pide. Poner xbpsOptimizeTrueColors hace que un libro Classic construya y aplique un plan fresco dentro de SaveAs, y 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 libro, así que consultas y guardados posteriores ven el mismo mapeo. Si el guardado falla, se cancela o levanta, los 56 colores originales y la generación original se restauran
Para reportes generados, encienda la política de optimización. Esos libros se construyen desde colores RGB y de tema, nadie afinó su paleta a mano, y un lector de solo índices los muestra mucho más cerca del original con ella. Conserve xbpsPreserve para libros cuya paleta es parte del contrato, como una plantilla que código posterior edita por ColorIndex, donde una ranura que cambia silenciosamente de color rompería ese código
Las fuentes XLSX toman otra ruta. SaveXLSXWorkbookAsXLS en lxXlsxExport construye un plan desde el TXLSXWorkbook cargado y lo escribe en la paleta destino antes de convertir cualquier estilo, así que cada celda convertida se iguala contra la paleta final. La demo del workbench de auditoría y conversión de libros corre exactamente esta ruta. Los colores de tema pasan por el mismo planificador una vez que su tint se resuelve a RGB; los rellenos de gráfico que deberían quedar como referencias de tema vivas son un caso aparte, cubierto en el artículo de rellenos de gráfico con colores de tema GelFrame, donde el XLS binario guarda un índice de esquema en lugar de un color aplanado
// Libro Classic: opt-in, aplica solo a guardados BIFF8
Workbook.BiffPaletteSavePolicy := xbpsOptimizeTrueColors;
if Workbook.SaveAs('report.xls', xlExcel97) <> 1 then
HandleSaveFailure; // su propio handler; la paleta ya fue restaurada
// Modelo XLSX a BIFF8 con un plan de paleta determinista
XWorkbook := TXLSXWorkbook.Create;
try
if XWorkbook.Open('report.xlsx') = 1 then
if SaveXLSXWorkbookAsXLS(XWorkbook, 'report.xls') <> 1 then
HandleSaveFailure;
finally
XWorkbook.Free;
end;
Las llamadas de paleta (ResolveIndexedColor, NearestIndexedColor, ScanIndexedColorUsage, BuildBiffPalettePlan y ApplyBiffPalettePlan) tienen las mismas firmas en IXLSWorkbook y TXLSXWorkbook, desde Delphi y C++Builder. El build de prueba de la página del componente Excel HotXLS para Delphi es suficiente para correr un plan contra su plantilla más colorida y leer MaxDistanceSquared antes de que se escriba nada